package com.jxmobi.util.ftp;

import com.jxmobi.util.exception.FTPSendException;
import com.jxmobi.util.exception.FTPTimeoutException;
import com.jxmobi.util.file.FileSplitterUtil;
import org.apache.commons.net.PrintCommandListener;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPConnectionClosedException;
import org.apache.commons.net.ftp.FTPReply;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.stream.Stream;

/**
 *  currently only support ftp (without support of ftps)
 *
 *  <strong>Example</strong>
 *  <pre>
 *       FtpSender.configure(new FtpEnvConfig("www,example.com", "ftpuser", "secret", FtpFileType.BINARY));
 *       FtpSender.sendFile("someFile", "someFolder/someFile2");
 *  </pre>
 *
 *     <strong>IMPORTANCES:</strong>
 *     By default, the FTP protocol establishes a data connection by opening a port on the client and allows the server connecting to this port.
 *  This is called local active mode, but it is usually blocked by firewall so the file transfer may not work.
 *  Fortunately, the FTP protocol has another mode, local passive mode, in which a data connection is made by opening a port on the server for the client to connect –
 *  and this is not blocked by firewall.
 *     So it is recommended to switch to local passive mode before transferring data, by invoking the method enterLocalPassiveMode() of the FTPClient class.
 *
 *   persuasive example:   http://www.codejava.net/java-se/networking/ftp/java-ftp-file-upload-tutorial-and-example
 *
 *  ftp server config http://www.journaldev.com/661/java-ftp-upload-example-using-apache-commons-net-api
 *
 *  http://stackoverflow.com/questions/14467107/java-accessing-a-file-from-an-ftp-server
 *
 *  http://www.codejava.net/java-se/networking/ftp/connect-and-login-to-a-ftp-server
 *
 *  http://www.liquidweb.com/kb/how-to-install-and-configure-vsftpd-on-centos-7/
 *
 *  configure vsftpd http://www.liquidweb.com/kb/how-to-install-and-configure-vsftpd-on-centos-7/
 *
 * @author Xiaofei Chen <a href="mailto:xchen@jxmobi.com">Email the author</a>
 * @version 1.0 5/25/16
 */
public class FtpSender {
    private static final Logger log = LoggerFactory.getLogger(FtpSender.class);

    private static FtpSender instance = new FtpSender();

    private FtpEnvConfig envConfig;

    private FTPClient ftpClient;

    /**
     *  初始化远程 ftp 服务器连接信息
     * @param config remote ftp server configuration
     * @return self
     */
    public static FtpSender configure(FtpEnvConfig config) {
        log.debug("setting the ftp env config, with config: " + config);
        instance.envConfig = config;
        instance.ftpClient = new FTPClient();
        return instance;
    }

    private FtpSender() {}


    public static void main(String[] args) {
        configure(new FtpEnvConfig("test.bimayun.com", "pwftp", "Qwerty555", FtpFileType.BINARY));
        sendFile("tcu_bin/TCU.bin", "v1.2.0/TCUv1-2-0.bin", true);
    }

    public static void sendFile(String localFile, boolean resource) {
        sendFile(localFile, localFile, resource);
    }

    /**
     *  批量上传文件到远程 ftp 服务器上
     * @param fileStream
     * @param parentPath
     * @param resource
     */
    public static String batchSendFilesOneLevel(Stream<File> fileStream, String parentPath, boolean resource) throws FTPSendException {
        establishTheFtpConnection();

        FTPClient ftpsClient = instance.ftpClient;

        String currentWorkDirectory = null;

        log.info("remote dir going to be created or include is " + parentPath);

        try {
            ftpCreateDirectoryTree(ftpsClient, parentPath);
            ftpsClient.changeWorkingDirectory(parentPath);
            currentWorkDirectory = ftpsClient.printWorkingDirectory();
            log.debug(" current working directory is " + currentWorkDirectory);
        } catch (IOException e) {
            e.printStackTrace();
        }

        fileStream.forEach(fileLocal -> {
            String file = FileSplitterUtil.retrieveResourcePath(fileLocal.getPath(), resource);

            log.info("current file or dir to be uploaded to the remote ftp server is " + file);

            FileInputStream fis = null;
            try {
                fis = new FileInputStream(file);

                log.info("current available is " + fis.available());

                String targetFile = fileLocal.getName();
                log.info("remote file going to be created is " + targetFile);

                ftpsClient.storeFile(targetFile, fis);
            } catch (IOException e) {
                log.error("存储本地文件到远程 ftp 服务器时发生错误，请检查设置");

                if ( e instanceof FTPConnectionClosedException)
                    throw new FTPTimeoutException(e.getMessage());

                throw new FTPSendException("file upload exception happens");


            } finally {
                if (fis != null) {
                    try {
                        fis.close();
                    } catch (IOException e) {
                        log.error("关闭文件输入流错误");
                        e.printStackTrace();
                    }
                }
            }
        });

        closeFtpConnection();

        return currentWorkDirectory;
    }


    /**
     *  批量上传文件到远程 ftp 服务器上
     * @param fileStream
     * @param startPath  full path
     * @param parentPath
     * @param resource
     */
    public static String batchSendFiles(Stream<File> fileStream, String startPath, String parentPath, boolean resource) {
        establishTheFtpConnection();

        FTPClient ftpsClient = instance.ftpClient;

        String startWorkDirectory = "";

        log.info("remote dir going to be created or include is " + parentPath);
        log.info("remote dir start is " + startPath);

        startWorkDirectory = remoteChangeWorkingDirectory(ftpsClient, parentPath);

        String fullPath = null;


        String finalStartWorkDirectory = startWorkDirectory;
        fileStream.forEach(fileLocal -> {
            String file = FileSplitterUtil.retrieveResourcePath(fileLocal.getPath(), resource);

            log.info("current file or dir to be uploaded to the remote ftp server is " + file);

            String remotePath = relativeDirectory(fileLocal.getPath(), startPath);
            System.err.println("######## " + remotePath);

            if (fileLocal.isDirectory()) {
                remoteChangeWorkingDirectory(ftpsClient, remotePath);
                return;
            }

            FileInputStream fis = null;
            try {
//                ftpsClient.changeWorkingDirectory(remotePath);

                fis = new FileInputStream(file);

                log.info("current available is " + fis.available());

                String targetFile = fileLocal.getName();
                log.info("remote file going to be created is " + targetFile);

                ftpsClient.storeFile(targetFile, fis);
            } catch (IOException e) {
                log.error("存储本地文件到远程 ftp 服务器时发生错误，请检查设置");
                e.printStackTrace();
            } finally {
                if (fis != null) {
                    try {
                        fis.close();
                    } catch (IOException e) {
                        log.error("关闭文件输入流错误");
                        e.printStackTrace();
                    }
                }
            }
        });

        closeFtpConnection();

        return startWorkDirectory;
    }

    private static String remoteChangeWorkingDirectory(FTPClient ftpsClient, String path) {
        String currentWorkDirectory = null;
        try {
            ftpCreateDirectoryTree(ftpsClient, path);
            log.info(" changing working direcory to " + path);
            currentWorkDirectory = ftpsClient.printWorkingDirectory();

            String childPath = relativeDirectory(path, currentWorkDirectory);
            String parentPath = relativeDirectory(currentWorkDirectory, path);

            if (childPath.equals("") && !parentPath.equals("")) {
                ftpsClient.changeToParentDirectory();
            }

            if (!childPath.equals("") && parentPath.equals("")) {
                ftpsClient.changeWorkingDirectory(childPath);
            }

//            ftpsClient.changeWorkingDirectory(path);

            log.debug(" current working directory is " + currentWorkDirectory);
            return currentWorkDirectory;
        } catch (IOException e) {
            e.printStackTrace();
        }

        return null;
    }

    /**
     *  retrieve the relative path of the originalPath compared to starterPath
     *
     *  eg. /foo/abc relative to /foo is abc
     * @param originalPath directory full path
     * @param starterPath directory full path
     * @return
     */
    private static String relativeDirectory(String originalPath, String starterPath) {
        int offset = originalPath.indexOf(starterPath);

        if (offset == -1) {
            return "";
        }

        String relativePath = originalPath.substring(offset);

        return relativePath;
    }

    private static void establishTheFtpConnection() {
        FtpEnvConfig config = instance.envConfig;

        if ( config == null) {
            log.error("请配置待 upload 的ftp服务器的相关配置信息（host,username,password）");
            return;
        }

        log.debug(config.toString());
        FTPClient ftpsClient = instance.ftpClient;
        FileInputStream fis = null;


        ftpsClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out)));

//            ftpsClient.setTrustManager(TrustManagerUtils.getAcceptAllTrustManager());
        int replyCode;

        try {
            ftpsClient.connect(config.getHost(), 21);
            replyCode = ftpsClient.getReplyCode();

            if (!FTPReply.isPositiveCompletion(replyCode)) {
                ftpsClient.disconnect();
                throw new RuntimeException(String.format("Exception in connecting to %s ", config.getHost()));
            }

            ftpsClient.login(config.getUsername(), config.getPass());
            ftpsClient.setDataTimeout(10 * 1000);
            ftpsClient.setSoTimeout(10 * 1000);

            // found this hanging bug @http://stackoverflow.com/questions/9706968/apache-commons-ftpclient-hanging
            log.debug("Default Buffer Size:" + ftpsClient.getSendDataSocketBufferSize());
            ftpsClient.setSendDataSocketBufferSize(1024 * 1024);

            ftpsClient.setFileType(config.getFileType());
            ftpsClient.enterLocalPassiveMode();
        } catch (IOException e) {
            log.error("建立远程 ftp 服务器链接失败，请检查设置，错误信息为：" + e.getMessage());
            e.printStackTrace();
        }

    }

    private static void closeFtpConnection() {
        try {
            FTPClient ftpsClient = instance.ftpClient;
            if (ftpsClient.isConnected()) {
                ftpsClient.setBufferSize(0);
                ftpsClient.logout();
                ftpsClient.disconnect();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     *  将本地文件发送到远程 ftp 服务器上，并以 destFile 命名
     *  destFile 可以包含目录，如果包含目录，该目录应该为相对目录
     *
     *  注意： 使用该方法之前，应该确保设置好 configure 方法
     * @param localFile 本地文件名
     * @param destFile 目标文件名（可以包含目录，如果包含，则应为相对目录）
     */
    public static void sendFile( String localFile, String destFile, boolean resource ) {
        FtpEnvConfig config = instance.envConfig;

        if ( config == null) {
            log.error("请配置待 upload 的ftp服务器的相关配置信息（host,username,password）");
            return;
        }

        log.debug(config.toString());
        FTPClient ftpsClient = instance.ftpClient;
        FileInputStream fis = null;

        try {
            ftpsClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out)));

//            ftpsClient.setTrustManager(TrustManagerUtils.getAcceptAllTrustManager());
            int replyCode;
            ftpsClient.connect(config.getHost(), 21);
            replyCode = ftpsClient.getReplyCode();

            if (!FTPReply.isPositiveCompletion(replyCode)) {
                ftpsClient.disconnect();
                throw new RuntimeException(String.format("Exception in connecting to %s ", config.getHost()));
            }

            ftpsClient.login(config.getUsername(), config.getPass());
            ftpsClient.setFileType(config.getFileType());
            ftpsClient.enterLocalPassiveMode();

            String file = FileSplitterUtil.retrieveResourcePath(localFile, resource);

            log.info("current file to be uploaded to the remote ftp server is " + file);

            fis = new FileInputStream(file);

            log.info("current available file size is " + fis.available());

            String destDir = destFile.substring(0, destFile.lastIndexOf("/"));
            String targetFile = destFile.substring(destFile.lastIndexOf("/") + 1);

            log.info("remote dir going to be created or include is " + destDir);
            log.info("remote file going to be created is " + targetFile);

            ftpCreateDirectoryTree(ftpsClient, destDir);

            ftpsClient.changeWorkingDirectory(destDir);

            ftpsClient.storeFile(targetFile, fis);


        } catch (IOException e) {
            log.error("ftp 发送指定文件" + localFile + "失败, 错误信息为：" + e.getMessage());
            e.printStackTrace();
        } finally {
            try {
                if (fis != null) {
                    fis.close();
                }

                if (ftpsClient.isConnected()) {
                    ftpsClient.logout();
                    ftpsClient.disconnect();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 在远程 Ftp 服务器上创建文件夹，如果已经存在，直接返回；否则，创建
     * @param client FTPClient
     * @param dirTree  the directory tree only delimited with / chars.  No file name!
     * @throws Exception
     */
    private static void ftpCreateDirectoryTree( FTPClient client, String dirTree ) throws IOException {

        boolean dirExists = true;

        //tokenize the string and attempt to change into each directory level.  If you cannot, then start creating.
        String[] directories = dirTree.split("/");
        for (String dir : directories ) {
            if (!dir.isEmpty() ) {
                if (dirExists) {
                    dirExists = client.changeWorkingDirectory(dir);
                }
                if (!dirExists) {
                    if (!client.makeDirectory(dir)) {
                        throw new IOException("Unable to create remote directory '" + dir + "'.  error='" + client.getReplyString()+"'");
                    }
                    if (!client.changeWorkingDirectory(dir)) {
                        throw new IOException("Unable to change into newly created remote directory '" + dir + "'.  error='" + client.getReplyString()+"'");
                    }
                }
            }
        }
    }
}
